本节将会对Java中内置的同步机制进行介绍,虽说是固有机制,但是如果滥用的话,还是会有不小的问题的。
synchronized关键字在Java中,关键字synchronized用于定义一个关键区,既可以是一段代码块,也可以是一个完整的方法,如下所示:
public synchronized void setGadget(Gadget g) {
this.gadget = g;
}
上面的方法定义中包含synchronized关键字,因此每次只能有一个线程修改给定对象的gadget域。
在同步方法方法中,监视器对象是隐式的,即当前对象,而对静态同步方法来说,监视器对象是当前对象的类对象。上面的示例代码与下面的代码是等效的:
public void setGadget(Gadget g) {
synchronized(this) {
this.gadget = g;
}
}
java.lang.Thread类Java中使用java.lang.Thread类表示对线程的抽象。相对于操作系统的具体实现,Thread类更具通用性,包含了启动线程和插入线程执行代码的基本方法,与之相似的是,操作系统的线程实现中,由线程创建者通过函数指针的形式指定新线程所要执行的代码。Java以面向对象的方式实现了相同的语义,任何类,只要实现了java.lang.Runnable接口,或继承java.lang.Thread类,均都可以成为一个线程,run方法的具体实现将是新线程的具体执行代码。
Java中的线程也有优先级概念,但是否真的起作用取决于JVM的具体实现。setPriority方法用于设置线程的优先级,提示JVM该线程更加重要或不怎么重要。当然,对于大多数JVM来说,显式的修改线程优先级没什么大帮助。当运行时"有把握"时,JRockit JVM甚至会忽略对该方法的调用。
正在运行的线程可以通过调用yield方法主动放弃剩余的时间片,以便其他线程运行,自身休眠(调用wait方法)或等待其他线程结束再运行(调用join方法)。
java.lang.ThreadGroup类(线程组)有点像类Unix系统中的进程,其中会包含多个线程,对线程组的操作会被应用到其中所有的线程上。
线程中使用java.lang.ThreadLocal来表示线程局部对象的数据,每个线程都有一份该数据的拷贝。该类自Java 1.2起引入,是一个非常有用的机制。对于不能在栈上分配局部对象的编程语言来说,这可说是个笨拙的改进,但的确可以提升应用程序的整体性能。如果程序员头脑清醒的话,善用ThreadLocal还是很有帮助的。
自诞生以来,java.lang.Thread类发生了很多变化,废弃了一些API,修改了一些方法实现。最初,Thread类中包含了用于终止、挂起和恢复线程的方法,但实践表明,使用这些方法有风险,但由于仍有人在使用这些方法,所以在本章后面的陷阱与伪优化一节中会详细介绍它们的危险性。
java.util.concurrent包JDK 1.5中引入的java.util.concurrent包中提供了对并发编程一些非常有用的数据结构和工具类,例如BlockingQueue类,该类是经典的生产者/消费者实现,当队列中空间不足时,就阻塞插入操作,当队列中没有元素时,就阻塞获取操作。
java.util.concurrent包使程序员无需再花费精力去"重新发明"做并发编程所需的基础数据结构,而且开发者对包中的类已经做了很多优化,使其具有良好的伸缩性和可用性。
此外,在其子包java.util.concurrent.atomic中包含了一些与原子操作相关的工具类,例如java.util.concurrent.atomic.AtomicInteger类和java.util.concurrent.atomic.AtomicLong类,可以以原子操作的形式及整型长整型数据进行计算。使用java.util.concurrent.atomic包可以避免在Java代码中显式的使用重量级锁。
最后,java.util.concurrent.locks包中有一些对常用锁的实现,例如读写锁,这样程序员就无需再花费精力去从头实现了。
读写锁不限制读操作,但写操作具有排他性。
信号量(semaphore)作为一种同步机制适用于以下场景,当一个线程试图获取某个资源时,却发现该资源已被其他线程持有,这时试图获取资源的线程会被挂起,直到资源持有者线程释放相关资源。信号量是锁的抽象,普遍存在与现代操作系统中,Java编程语言也对其提供了完整的支持。
在Java中,每个对象都继承自java.lang.Object类,有wait notify和notifyAll这几个方法,可以用于实现信号量的语义,只不过它们需要用在监视器对象上下文中,例如用在synchronized代码块中,否则JVM会抛出IllegalMonitorStateException异常。
调用wait方法会将当前线程挂起,当接收到监视器的通知时会被唤醒。当调用notify方法时,会根据JVM中的线程调度算法,从阻塞在对应监视器上的线程中选择一个,将之唤醒,使其继续执行。当调用notifyAll方法时,所有阻塞在监视器上的线程都会被唤醒,但只有一个会成功得到锁,其余的线程会再次被挂起。相对于notify方法来说,notifyAll方法更安全,但执行开销更大一些。所以,除非有把握,应尽量避免使用notifyAll方法。
调用wait方法时,可以附加一个时间参数,用于指定线程被挂起的超时时间,超过该时间后,线程会自动醒来。
下面的代码是一个生产者/消费者模型示例,用于展示如何在Java中使用信号量,使用了隐式监视器来进行同步控制。
public class Mailbox {
private String message;
private boolean messagePending;
/**
* Places a message in the mailbox
*/
public synchronized void putMessage(String message) {
while (messagePending) { //wait for consumers to consume
try {
wait(); //blocks until notified
} catch (InterruptedException e) {
}
}
this.message = message; //store message in mailbox
messagePending = true; //raise flag on mailbox
notifyAll(); //wake up any random consumer
}
/**
* Retrieves a message from the mailbox
*/
public synchronized String getMessage() {
while (!messagePending) { //wait for producer to produce
try {
wait(); //blocks until notified
} catch (InterruptedException e) {
}
}
messagePending = false; //lower flag on mailbox
notifyAll(); //wake up any random producer
return message;
}
}
在上面的代码中,多个生产者线程和消费者线程可以使用同一个Mailbox对象来同步控制消息的发送和接收。当消费者线程调用getMessage方法时,如果Mailbox中没有消息存在,则调用线程会被挂起,直到生产者线程调用putMessage方法填入一个消息。同样的,当生产者线程调用putMessage方法时,若发现邮箱已满,则会被挂起,直到消费者线程取走消息。
上面是简化过的生产者/消费者模型,信号量的选择可以是布尔值,也可以是数值。在
Mailbox类中,信号量就是一个布尔值,以true或false来控制对单一资源的访问。数值的信号量可以用来限制资源访问者的数量。具体实现可以参考java.util.concurrent.Semaphore类。
volatile关键字在多线程环境下,对某个属性域或内存地址进行写操作后,其他正在运行的线程未必能立即看到这个结果。本节将对这个问题做详细介绍,并在后面的4.3.1节中对Java内存模型进行介绍。确实有些场景要求所有线程在执行时需要得知某个属性域的值,为此,Java提供了关键字volatile来解决此问题。
使用volatile修饰属性域的声明可以保证对该属性域的写操作会直接作用到内存中。原本,数据操作仅仅将数据写到CPU缓存中,过一会再写到内存中,正因如此,在同一个属性域上,不同的线程可能看到不同的值。目前,JVM在实现volatile关键字时,是通过JIT在存储属性域操作后插入内存屏障代码来实现的,只不过这种方法有一点性能损耗。
人们常常难以理解“为什么不同的线程会在同一个属性域上看到不同的值”,这种情况不同常见。一般来说这是因为,目前的机器的内存模型已经足够强,又或者应用程序的本身结构就不容易使非volatile域出现这个问题。但是,考虑到JIT优化编译器可能会对程序做较大改动,如果开发人员不留心的话,还是会出现问题的。下面的示例代码解释了在Java程序中,为什么内存语义如此重要,当问题的来龙去脉还不清楚的时候,尤其重要。
public class MyThread extends Thread {
private volatile boolean finished;
public void run() {
while (!finished) {
//
}
}
public void signalDone() {
this.finished = true;
}
}
如果用定义变量finished时没有加上volatile关键字,那么在理论上,JIT编译器在进行优化时,可能会将之修改为只在循环开始前加载一次finished域的值,但这样的话就改变代码原本的含义。这时,如果finished域的值是false,那么程序就会陷入无限循环,即使其他线程调用了signalDone方法也没用。Java语言规范写明,如果编译器认为合适的话,可以为非volatile变量在线程内创建拷贝以便后续使用。
下面代码进一步描述了volatile关键字的含义:
public class Test {
volatile int a = 1;
volatile int b = 1;
void add() {
a++;
b++;
}
void print() {
System.out.println(a + " " + b);
}
}
上面的代码中,关键字volatile隐式的保证了,即使在多线程环境下,变量b永远不会比变量a大。如果给add方法定义加上synchronized关键字的话,那么在调用print方法时,打印出的a和b永远都是相等的。如果变量都没有使用volatile声明,add方法也没有使用synchronized声明,那么按照Java语言规范来说,变量a和变量b就不一定谁大谁小了。
由于一般会使用内存屏障来实现
volatile关键字的语义,会导致CPU缓存失效,降低应用程序整体性能,因此使用的时候要谨慎。
一般来说,同步操作的开销会比非同步操作大,因此,程序员应该在不违反内存语义的前提下,考虑使用其他可以传递数据的方法,而不是动不动就把volatile和synchronized搬出来。